package com.atguigu.gmall.list.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.list.service.SearchService;
import com.atguigu.gmall.list.repsitory.GoodsRepsitory;
import com.atguigu.gmall.model.list.*;
import com.atguigu.gmall.model.product.BaseAttrInfo;
import com.atguigu.gmall.model.product.BaseCategoryView;
import com.atguigu.gmall.model.product.BaseTrademark;
import com.atguigu.gmall.model.product.SkuInfo;
import com.atguigu.gmall.product.client.ProductFeignClient;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.stream.Collectors;

@Service
public class SearchServiceImpl implements SearchService {

    //  服务层 需要调用service-product 这个微服务的远程调用。
    @Autowired
    private ProductFeignClient productFeignClient;

    //  GoodsRepsitory 自己创建的一个接口
    @Autowired
    private GoodsRepsitory goodsRepsitory;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Override
    public void upperGoods(Long skuId) {
        //  本质 给Goods 赋值，然后将Goods 保存到es索引库中.
        Goods goods = new Goods();

        //  还可以优化： 可以使用异步编排 CompletableFuture
        //  根据skuId 来获取skuInfo
        SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId); // {可能会走缓存，如果缓存中的数据是最新那就没事.}
        goods.setId(skuId);
        goods.setTitle(skuInfo.getSkuName());
        goods.setDefaultImg(skuInfo.getSkuDefaultImg());
        //  goods.setPrice(skuInfo.getPrice().doubleValue()); // 有选择：选择有缓存的数据,
        goods.setPrice(productFeignClient.getSkuPrice(skuId).doubleValue()); // 有选择：查询的是数据
        goods.setCreateTime(new Date());

        //  赋值品牌数据
        BaseTrademark trademark = productFeignClient.getTrademark(skuInfo.getTmId());
        goods.setTmId(trademark.getId());
        goods.setTmName(trademark.getTmName());
        goods.setTmLogoUrl(trademark.getLogoUrl());

        //  赋值分类数据.
        BaseCategoryView categoryView = productFeignClient.getCategoryView(skuInfo.getCategory3Id());
        goods.setCategory1Id(categoryView.getCategory1Id());
        goods.setCategory2Id(categoryView.getCategory2Id());
        goods.setCategory3Id(categoryView.getCategory3Id());

        goods.setCategory1Name(categoryView.getCategory1Name());
        goods.setCategory2Name(categoryView.getCategory2Name());
        goods.setCategory3Name(categoryView.getCategory3Name());

        //  商品的热度排名初始化值 都是 0

        //  赋值平台属性平台属性值
        //  List<SearchAttr> attrsList = new ArrayList<>();
        List<BaseAttrInfo> attrList = this.productFeignClient.getAttrList(skuId);

        //  循环遍历获取对应的数据：

        List<SearchAttr> attrsList = attrList.stream().map(baseAttrInfo -> {
            SearchAttr searchAttr = new SearchAttr();
            searchAttr.setAttrId(baseAttrInfo.getId());
            searchAttr.setAttrName(baseAttrInfo.getAttrName());
            searchAttr.setAttrValue(baseAttrInfo.getAttrValueList().get(0).getValueName());
            //  返回检索对应的平台属性对象.
            return searchAttr;
        }).collect(Collectors.toList());

        //  将平台属性集合对象赋值给goods
        goods.setAttrs(attrsList);

        //  将goods 保存到es 中！
        this.goodsRepsitory.save(goods);
    }

    @Override
    public void lowerGoods(Long skuId) {
        //  商品下架方法实现：
        this.goodsRepsitory.deleteById(skuId);
    }

    @Override
    public SearchResponseVo search(SearchParam searchParam) throws Exception {
        //  声明一个对象
        SearchResponseVo searchResponseVo = new SearchResponseVo();
        /*
         private List<SearchResponseTmVo> trademarkList;
         private List<SearchResponseAttrVo> attrsList = new ArrayList<>();
         private List<Goods> goodsList = new ArrayList<>();
         private Long total;//总记录数
         private Integer pageSize;//每页显示的内容
         private Integer pageNo;//当前页面
         private Long totalPages;
         */
        //  准备好dsl 语句.
        SearchRequest searchRequest = this.buildQueryDsl(searchParam);
        //  执行dsl 语句，
        SearchResponse searchResponse = this.restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        //  获取到返回结果集，  searchResponse ----> searchResponseVo
        /*
         在这个方法中给 以下属性赋值。
         private List<SearchResponseTmVo> trademarkList;
         private List<SearchResponseAttrVo> attrsList = new ArrayList<>();
         private List<Goods> goodsList = new ArrayList<>();
         private Long total;//总记录数
         */
        searchResponseVo = this.parseSearchResult(searchResponse);

        searchResponseVo.setPageNo(searchParam.getPageNo()); // 表示当前页 1
        searchResponseVo.setPageSize(searchParam.getPageSize()); // 表示每页显示的条数 3
        //  9 3 3 | 10 3 4;
        //  Long totalPages = searchResponseVo.getTotal()%searchParam.getPageSize()==0?searchResponseVo.getTotal()/searchParam.getPageSize():searchResponseVo.getTotal()/searchParam.getPageSize()+1;
        //  开发中总结的公式：
        Long totalPages = (searchResponseVo.getTotal()+searchParam.getPageSize()-1)/searchParam.getPageSize();
        searchResponseVo.setTotalPages(totalPages); // 表示总页数！ 有当前总条数 ，当前每页显示大小，才能计算总页数!
        //  返回数据.
        return searchResponseVo;
    }

    /**
     * 转换结果集方法.
     * @param searchResponse
     * @return
     */
    private SearchResponseVo parseSearchResult(SearchResponse searchResponse) {
        SearchResponseVo searchResponseVo = new SearchResponseVo();
        /*
         private List<SearchResponseTmVo> trademarkList;
         private List<SearchResponseAttrVo> attrsList = new ArrayList<>();
         private List<Goods> goodsList = new ArrayList<>();
         private Long total;//总记录数
         */
        //  hits" : {
        //    "total" : {
        //      "value" : 4,
        //      "relation" : "eq"
        //    },
        SearchHits hits = searchResponse.getHits();
        //  获取总记录数据.
        searchResponseVo.setTotal(hits.getTotalHits().value);

        //  赋值 goodsList
        List<Goods> goodsList = new ArrayList<>();
        //  获取到 subHits 数据
        SearchHit[] subHits = hits.getHits();
        if (subHits!=null && subHits.length>0){
            //  循环遍历
            for (SearchHit subHit : subHits) {
                //  获取到_source 对应的数据.
                String sourceAsString = subHit.getSourceAsString();
                //  将字符串转化为Goods
                Goods goods = JSON.parseObject(sourceAsString, Goods.class);
                //  细节：如果用户通过检索关键词 ，则需要获取到高亮的商品名称.
                if (subHit.getHighlightFields().get("title")!=null){
                    //  如果不为空，则说明是有高亮显示. 将原来的title 覆盖.
                    Text title = subHit.getHighlightFields().get("title").getFragments()[0];
                    goods.setTitle(title.toString());
                }
                //  将goods 添加到集合
                goodsList.add(goods);
            }
        }
        //  商品集合赋值完成.
        searchResponseVo.setGoodsList(goodsList);

        //  赋值品牌数据：
        //  List<SearchResponseTmVo> trademarkList = new ArrayList<>();
        //  空数据. trademarkList 赋值.
        //  获取聚合中对应的品牌数据.
        Map<String, Aggregation> aggregationMap = searchResponse.getAggregations().asMap();
        //  获取到品牌Id 聚合对象。
        ParsedLongTerms tmIdAgg = (ParsedLongTerms) aggregationMap.get("tmIdAgg");
        //  赋值品牌数据：
        List<SearchResponseTmVo> trademarkList = tmIdAgg.getBuckets().stream().map(bucket -> {
            //  创建品牌对象
            SearchResponseTmVo searchResponseTmVo = new SearchResponseTmVo();
            //  获取到了品牌Id "key" : 1
            String keyAsString = ((Terms.Bucket) bucket).getKeyAsString();
            searchResponseTmVo.setTmId(Long.parseLong(keyAsString));

            //  获取到品牌名称.
            ParsedStringTerms tmNameAgg = ((Terms.Bucket) bucket).getAggregations().get("tmNameAgg");
            String tmName = tmNameAgg.getBuckets().get(0).getKeyAsString();
            searchResponseTmVo.setTmName(tmName);

            //  获取到品牌url
            ParsedStringTerms tmLogoUrlAgg = ((Terms.Bucket) bucket).getAggregations().get("tmLogoUrlAgg");
            String tmLogoUrl = tmLogoUrlAgg.getBuckets().get(0).getKeyAsString();
            searchResponseTmVo.setTmLogoUrl(tmLogoUrl);
            //  返回品牌对象.
            return searchResponseTmVo;
        }).collect(Collectors.toList());
        searchResponseVo.setTrademarkList(trademarkList);

        //  赋值平台属性： 数据类型：nested.
        //  List<SearchResponseAttrVo> attrsList = new ArrayList<>();
        //  获取到数据. nested
        ParsedNested attrAgg = (ParsedNested) aggregationMap.get("attrAgg");
        //  nested aggs
        ParsedLongTerms attrIdAgg = attrAgg.getAggregations().get("attrIdAgg");
        List<SearchResponseAttrVo> attrsList = attrIdAgg.getBuckets().stream().map(bucket -> {
            //  创建对象
            SearchResponseAttrVo searchResponseAttrVo = new SearchResponseAttrVo();
            //  赋值平台属性Id
            String keyAsString = ((Terms.Bucket) bucket).getKeyAsString();
            //            Number keyAsNumber = ((Terms.Bucket) bucket).getKeyAsNumber();
            //            searchResponseAttrVo.setAttrId(keyAsNumber.longValue());
            searchResponseAttrVo.setAttrId(Long.parseLong(keyAsString));

            //  赋值平台属性名： 在集合中都是一个值.
            ParsedStringTerms attrNameAgg = ((Terms.Bucket) bucket).getAggregations().get("attrNameAgg");
            String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
            searchResponseAttrVo.setAttrName(attrName);

            //  赋值平台属性值名： 不是一个数据了，因为一个属性 下 对 多个属性值  1：n
            ParsedStringTerms attrValueAgg = ((Terms.Bucket) bucket).getAggregations().get("attrValueAgg");
            //  对它不熟悉......   Terms.Bucket::getKeyAsString 根据key获取到数据。
            List<String> attrValueList = attrValueAgg.getBuckets().stream().map(Terms.Bucket::getKeyAsString).collect(Collectors.toList());
            //  第二种：
            //            ArrayList<String> strings = new ArrayList<>();
            //            for (Terms.Bucket attrValueAggBucket : attrValueAgg.getBuckets()) {
            //                String attrValue = attrValueAggBucket.getKeyAsString();
            //                strings.add(attrValue);
            //            }
            //            searchResponseAttrVo.setAttrValueList(strings);
            searchResponseAttrVo.setAttrValueList(attrValueList);

            //  返回数据.
            return searchResponseAttrVo;
        }).collect(Collectors.toList());

        searchResponseVo.setAttrsList(attrsList);
        //  返回对象.
        return searchResponseVo;
    }

    /**
     * 动态生成dsl 语句
     * @param searchParam
     * @return
     */
    private SearchRequest buildQueryDsl(SearchParam searchParam) {
        //  声明一个查询器：{}
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //  声明一个QueryBuild 对象 {query bool}
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //  判断用户是否根据分类Id 过滤
        if (!StringUtils.isEmpty(searchParam.getCategory1Id())){
            // {query bool filter term }
            boolQueryBuilder.filter(QueryBuilders.termQuery("category1Id",searchParam.getCategory1Id()));
        }
        if (!StringUtils.isEmpty(searchParam.getCategory2Id())){
            // {query bool filter term }
            boolQueryBuilder.filter(QueryBuilders.termQuery("category2Id",searchParam.getCategory2Id()));
        }

        if (!StringUtils.isEmpty(searchParam.getCategory3Id())){
            // {query bool filter term }
            boolQueryBuilder.filter(QueryBuilders.termQuery("category3Id",searchParam.getCategory3Id()));
        }

        //  判断是否根据关键词检索！
        if (!StringUtils.isEmpty(searchParam.getKeyword())){
            // {query match } 将分词之后的数据取 and
            MatchQueryBuilder title = QueryBuilders.matchQuery("title", searchParam.getKeyword()).operator(Operator.AND);
            // {query bool must match }
            //  如果有过滤，同时又有了检索。
            boolQueryBuilder.must(title);

            //  高亮：如果关键词不为空，可能有高亮设置。
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            highlightBuilder.field("title");
            highlightBuilder.preTags("<span style=color:red>");
            highlightBuilder.postTags("</span>");
            searchSourceBuilder.highlighter(highlightBuilder);

        }

        //  根据品牌Id 进行过滤. trademark=1:小米 传递的参数会映射到实体类
        String trademark = searchParam.getTrademark();
        if (!StringUtils.isEmpty(trademark)){
            //  用户点击了品牌进行过滤。  1:小米
            String[] split = trademark.split(":");
            if (split!=null && split.length==2){
                // {query bool filter term}
                boolQueryBuilder.filter(QueryBuilders.termQuery("tmId",split[0]));
            }
        }

        //  根据平台属性值过滤. &props=23:6G:运行内存&props=24:256G:机身内存
        String[] props = searchParam.getProps();
        if (props!=null && props.length>0){
            //  循环遍历这个数组
            for (String prop : props) {
                // prop 表示数组中的单个对象数据. 23:6G:运行内存
                String[] split = prop.split(":");
                if (split!=null && split.length==3){
                    //  创建boolQuery
                    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
                    BoolQueryBuilder subBoolQuery = QueryBuilders.boolQuery();
                    // subBoolQuery.filter(QueryBuilders.termQuery("attrs.attrId",split[0]));
                    // subBoolQuery.filter(QueryBuilders.termQuery("attrs.attrValue",split[1]));
                    subBoolQuery.must(QueryBuilders.termQuery("attrs.attrId",split[0]));
                    subBoolQuery.must(QueryBuilders.termQuery("attrs.attrValue",split[1]));
                    // 写入nested！
                    boolQuery.must(QueryBuilders.nestedQuery("attrs",subBoolQuery, ScoreMode.None));
                    //  将内层的查询结构赋值给外出的bool
                    boolQueryBuilder.filter(boolQuery);
                }
            }
        }
        //  query 查询
        searchSourceBuilder.query(boolQueryBuilder);
        //  分页：
        int from = (searchParam.getPageNo()-1)*searchParam.getPageSize();
        searchSourceBuilder.from(from); // 从第几条数据开始查询.
        searchSourceBuilder.size(searchParam.getPageSize());

        //  排序：价格 order=2:desc  order=2:asc   综合 order=1:desc  order=1:asc
        String order = searchParam.getOrder();
        if (!StringUtils.isEmpty(order)){
            //  分割
            String[] split = order.split(":");
            String field = "";
            if (split!=null && split.length==2){
                switch (split[0]){
                    case "1":
                        field = "hotScore";
                        break;
                    case "2":
                        field = "price";
                        break;
                }
                searchSourceBuilder.sort(field,"asc".equals(split[1])? SortOrder.ASC:SortOrder.DESC);
            }else {
                //  默认排序规则：
                searchSourceBuilder.sort("hotScore",SortOrder.DESC);
            }
        }

        //  聚合：品牌
        searchSourceBuilder.aggregation(AggregationBuilders.terms("tmIdAgg").field("tmId")
                .subAggregation(AggregationBuilders.terms("tmNameAgg").field("tmName"))
                .subAggregation(AggregationBuilders.terms("tmLogoUrlAgg").field("tmLogoUrl"))
        );

        //  聚合：平台属性：数据类型nested！
        searchSourceBuilder.aggregation(AggregationBuilders.nested("attrAgg","attrs")
                        .subAggregation(AggregationBuilders.terms("attrIdAgg").field("attrs.attrId")
                        .subAggregation(AggregationBuilders.terms("attrNameAgg").field("attrs.attrName"))
                         .subAggregation(AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue"))
                        )
        );

        //  设置好索引库！ GET /goods/_search
        SearchRequest searchRequest = new SearchRequest("goods");
        //  将{} 添加到searchRequest 中.
        searchRequest.source(searchSourceBuilder);
        //  表示过滤字段用的.
        searchSourceBuilder.fetchSource(new String[]{"id","defaultImg","title","price","createTime"},null);

        //  可以通过打印的方式：获取到系统 dsl 语句
        System.out.println("DSL:\t"+searchSourceBuilder.toString());
        //  返回对象。
        return searchRequest;
    }

    @Override
    public void incrHotScore(Long skuId) {
        //  本质就是更高 es 中 hotScore 字段的数据.  每访问一次，更新一次hotScore 。这样的话，会有性能伤害！ 每次更新的时候都有io操作.
        //  借助redis 记录当前这个商品被访问的次数，如果这个访问次数，达到一定的数量if(count%10==0){则去更新一下 es！}，
        //  确定好使用哪个数据类型。 ZSet; 选择ZSet！
        //  ZINCRBY hotscore 1 23
        String hotKey = "hotScore";
        Double count = this.redisTemplate.opsForZSet().incrementScore(hotKey, "skuId:" + skuId, 1);

        //  判断
        if (count%10==0){
            //  更新一下es
            Optional<Goods> optional = this.goodsRepsitory.findById(skuId);
            Goods goods = optional.get();
            goods.setHotScore(count.longValue());
            //  将更新之后的数据放入es.
            this.goodsRepsitory.save(goods);
        }

    }


}
